/**
 * Software License, Version 1.0
 * 
 * Copyright 2003 The Trustees of Indiana University.  All rights reserved.
 * 
 *
 *Redistribution and use in source and binary forms, with or without 
 *modification, are permitted provided that the following conditions are met:
 *
 *1) All redistributions of source code must retain the above copyright notice,
 * the list of authors in the original source code, this list of conditions and
 * the disclaimer listed in this license;
 *2) All redistributions in binary form must reproduce the above copyright 
 * notice, this list of conditions and the disclaimer listed in this license in
 * the documentation and/or other materials provided with the distribution;
 *3) Any documentation included with all redistributions must include the 
 * following acknowledgement:
 *
 *"This product includes software developed by the Community Grids Lab. For 
 * further information contact the Community Grids Lab at 
 * http://communitygrids.iu.edu/."
 *
 * Alternatively, this acknowledgement may appear in the software itself, and 
 * wherever such third-party acknowledgments normally appear.
 * 
 *4) The name Indiana University or Community Grids Lab or NaradaBrokering, 
 * shall not be used to endorse or promote products derived from this software 
 * without prior written permission from Indiana University.  For written 
 * permission, please contact the Advanced Research and Technology Institute 
 * ("ARTI") at 351 West 10th Street, Indianapolis, Indiana 46202.
 *5) Products derived from this software may not be called NaradaBrokering, 
 * nor may Indiana University or Community Grids Lab or NaradaBrokering appear
 * in their name, without prior written permission of ARTI.
 * 
 *
 * Indiana University provides no reassurances that the source code provided 
 * does not infringe the patent or any other intellectual property rights of 
 * any other entity.  Indiana University disclaims any liability to any 
 * recipient for claims brought by any other entity based on infringement of 
 * intellectual property rights or otherwise.  
 *
 *LICENSEE UNDERSTANDS THAT SOFTWARE IS PROVIDED "AS IS" FOR WHICH NO 
 *WARRANTIES AS TO CAPABILITIES OR ACCURACY ARE MADE. INDIANA UNIVERSITY GIVES
 *NO WARRANTIES AND MAKES NO REPRESENTATION THAT SOFTWARE IS FREE OF 
 *INFRINGEMENT OF THIRD PARTY PATENT, COPYRIGHT, OR OTHER PROPRIETARY RIGHTS. 
 *INDIANA UNIVERSITY MAKES NO WARRANTIES THAT SOFTWARE IS FREE FROM "BUGS", 
 *"VIRUSES", "TROJAN HORSES", "TRAP DOORS", "WORMS", OR OTHER HARMFUL CODE.  
 *LICENSEE ASSUMES THE ENTIRE RISK AS TO THE PERFORMANCE OF SOFTWARE AND/OR 
 *ASSOCIATED MATERIALS, AND TO THE PERFORMANCE AND VALIDITY OF INFORMATION 
 *GENERATED USING SOFTWARE.
 */
package cgl.narada.protocol;

import java.util.Hashtable;

import cgl.narada.util.ByteUtilities;
/**
 * NodeAdditionProtocol.java 
 * This call is responsible for the node addtion protocol. Specific
 * responsibilities for this class include the following
 *<ul>
 * <li> Handle the requests for node additions
 *   <ul> Consistent with the connection vector semantics and the IP 
 *       discrimination scheme.</ul>
 * <li> Construct replies to the node in question.
 *  <ul><li> This could include providing a list of gateways to the nodes, 
 *       in case the requesting node seeks a TO relationship with the node 
 *       in question
 *  <li>Replies should indicate the status of the request, whether it failed,
 *      succeeded or if its simply giving a list of other gateways that the 
 *      requesting node may try to connect to. </ul>
 * <li> Handle replies
 *<ul><li> In case of success, update the logical address
 *   <li> In the case of request forwarding, construct a new request to the
 *        client in the original reply.
 *   <li> In case of failures, initiate strategy to locate a new node.
 *   <li> This is THE MOST IMPORTANT - In the case of a success, all the nodes
 *        need to have their logical addressess updated, this would call for 
 *        also updating the addresses of the client nodes that have been 
 *        attached to the server nodes.</ul>
 *</ul> 
 * @author Shrideep Pallickara
 * $Revision$
 * $Date$
 * 
 */

public class NodeAdditionProtocol implements ProtocolDebugFlags {
  /** The first element represents the # of open connections allowed as a 
      level-0 gateway, and so on. */
  private short[] connectionVector;
  
  /** Indicates the number of connections so far at each level */
  private short[] connectionsSoFar;
  
  /** The IP discrimination Scheme */
  private String ipDiscriminator;
  
  /** NodeAddress */
  private NodeAddress nodeAddress;

  /** The System level */
  private int systemLevel;
  /** This maintains information regarding wether this node can be used to
      assign addresses at various levels. We usually reserve this right to 
      the lowest numbered unit within a super-unit. */
  private boolean[] addressAssignerAtLevel;
  
  /** This maintain information regarding the number of units within each 
      context of the node. */
  private int[] numberOfUnitsAtLevel;
  
  /** GatewayInfo:  Maintains information about revelant gatekeepers within
      the system */
  private GatewayInfo gatewayInfo; 
  
  private ProtocolHandler protocolHandler;

  private byte[] addressBytes;

  private Destinations thisDestination;

  private Hashtable connectionRequestTable;
  
  private NodeAdditionProtocolHelper helper;

  private String moduleName = "NodeAdditionProtocol: ";

  public NodeAdditionProtocol() {}
  
  public NodeAdditionProtocol(NodeAddress nodeAddress,
			      short[] connectionVector,
			      String ipDiscriminator, 
			      GatewayInfo gatewayInfo) {

    this.nodeAddress = nodeAddress;
    this.connectionVector = connectionVector;
    this.ipDiscriminator = ipDiscriminator;
    this.gatewayInfo = gatewayInfo;
    addressBytes = nodeAddress.getAddressInBytes();
    /** Helps us compute a new destination for forwarding the address request
	and also lets us know if we have arrived at our destination */
    thisDestination = new Destinations(addressBytes);
    connectionRequestTable = new Hashtable();
    initializeProtocolArrays(nodeAddress);
    helper = new NodeAdditionProtocolHelper();
  }

  public void setProtocolHandler(ProtocolHandler protocolHandler) {
    this.protocolHandler = protocolHandler;
  }

  public void setNodeAddress(NodeAddress nodeAddress) {
    this.nodeAddress = nodeAddress;
    addressBytes = nodeAddress.getAddressInBytes();
    /** Helps us compute a new destination for forwarding the address request
	and also lets us know if we have arrived at our destination */
    thisDestination = new Destinations(addressBytes);
    initializeProtocolArrays(nodeAddress);
  }
  
  private void initializeProtocolArrays(NodeAddress nodeAddress) {
    int[] address = nodeAddress.getAddressInInts();
    systemLevel = address.length-1;
    int addressLength = address.length;
    /** Initialize the connectionsSoFar array */
    connectionsSoFar = new short[addressLength];
    numberOfUnitsAtLevel = new int[addressLength];
    
    for (int i =0; i < addressLength; i++) {
      connectionsSoFar[i] = (short)0;
      numberOfUnitsAtLevel[i] = 0;
    }
    
    /** Initialize the assigner array */
    addressAssignerAtLevel = new boolean[addressLength];
    for (int i=0; i < addressLength; i++) {
      //if (address[i] != 1) {
      //addressAssignerAtLevel[i] = false;
      //} else {
      //addressAssignerAtLevel[i] = true;
	numberOfUnitsAtLevel[i] = 1;
	//}
    }
  }
  
  /** This method sets this node to be the node assigner at a certain level.
      @param level The level for which this node is the address assigner.
   */
  public void setAddressAssignerForLevel(int level) {
    addressAssignerAtLevel[level] = true;
  }
  
  /** Not all addresses can be assigned at the node which has received a node
      addition request. This request thus needs to be forwarded to the node
      which is responsible for assigning addresses within a certain level 
      
      As a general rule 1.1.1.1 is responsible for the assigning addresses to
      clusters in 1.1.1 and super clusters in 1.1 ....
      while 28.29.23.1 is responsible for assigning addresses to nodes in the
      cluster 28.29.23 
      <i>level</i> indicates a request for assigning the address at level-l
      This also rules out sending this request over hops with level > than 
      <i>level</i>.
      <i>fromAddress</i> - This includes routing information from level-0
      upwards till <i>level</i> for which an address is requested. 
  */
  public void processNodeAddressRequest(byte[] nodeAddressRequest) {
    int offset =0;
    offset++;
    int requestedLevel = nodeAddressRequest[offset++];
    /** skipping the requestId for now */
    byte[] idBytes = new byte[10];
    System.arraycopy(nodeAddressRequest, offset, idBytes, 0, 10);
    ProtocolID requestId = new ProtocolID(idBytes);
    offset+=10;
    byte[] origBytes = new byte[4*(systemLevel+1)];
    System.arraycopy(nodeAddressRequest, offset, 
		     origBytes, 0, 4*(systemLevel+1));
    Destinations originated =  new Destinations(origBytes);
    offset+=(4*(systemLevel+ 1));
    byte[] destBytes = new byte[4*(systemLevel+1)];
    System.arraycopy(nodeAddressRequest, offset, 
		     destBytes, 0, 4*(systemLevel+1));
    Destinations toReach = new Destinations(destBytes);
    
    offset+=(4*(systemLevel+ 1));
    byte[] soFarBytes = new byte[4*(systemLevel+1)];
    System.arraycopy(nodeAddressRequest, offset, 
		     soFarBytes, 0, 4*(systemLevel+1));
    Destinations reachedSoFar = new Destinations(soFarBytes);
    System.out.println("Node Address Request Received for level (" +
		       requestedLevel +") node. The request originated at " + 
		       originated + " and the destination to reach computed " +
		       "so far is " + toReach);
    
    handleNodeAddressRequest(requestedLevel, requestId, originated, 
			     toReach, reachedSoFar);
  }

  private void handleNodeAddressRequest(int requestedLevel,
					ProtocolID requestId, 
					Destinations origin,
					Destinations toReach,
					Destinations reachedSoFar) {
    toReach =
    generateDestinationForNodeAdditionAtLevel(requestedLevel);
    /** Check to see if you have indeed arrived at the destination which
	could assign an address for you */
    if (toReach.equals(thisDestination)) {
       /** Put check here to see if the number of units exceeds 31*/
      //numberOfUnitsAtLevel[requestedLevel]++;
      //System.out.println("Num Of units at level (" +  requestedLevel + 
      //		 ")->" +  numberOfUnitsAtLevel[requestedLevel]);
      //toReach.setDestinationsAtLevel(requestedLevel, 
      //		     numberOfUnitsAtLevel[requestedLevel]);

      int assignedUnitId = assignAddressForUnitAtLevel(requestedLevel);
      toReach.setDestinationsAtLevel(requestedLevel, assignedUnitId );

      if (requestedLevel > 0) {
	for (int i = requestedLevel-1; i >= 0; i--) {
	  toReach.setDestinationsAtLevel(i, 1);
	}
      }
      //byte[] addressBytes = toReach.getDestinationsInBytes();
      //NodeAddress addressAssigned = new NodeAddress(addressBytes);
      NodeAddress addressAssigned = 
	new NodeAddress(toReach.getDestinationsInBytes());
      
      byte[] nodeAddressResponse = 
	helper.constructNodeAddressResponse(requestedLevel, systemLevel, 
					    requestId, addressAssigned, 
					    nodeAddress, 
					    origin, thisDestination);
      System.out.println("NAP::Address assigned is " + addressAssigned + 
			 "ToReach " + origin + 
			 "TraversedSoFar" + thisDestination);
      sendToDestination(origin, thisDestination, nodeAddressResponse);
      return;
    }
    reachedSoFar.markAsTraversedAt(thisDestination);
    forwardNodeAddressRequest(requestedLevel, requestId, 
			      origin, toReach, reachedSoFar);
  }
  
  public void processNodeAddressResponse(byte[] nodeAddressResponse) {
    int offset = 
      1 /*header*/ + 1 /*requested Level*/ + 10 /*requestId */+
      4*(systemLevel+1) /** The assigned address */ + 
      4*(systemLevel+1) /** The address assigned by */;
    byte[] toReachBytes = new byte[4*(systemLevel+1)];
    System.arraycopy(nodeAddressResponse, offset, 
		     toReachBytes, 0, 4*(systemLevel+1));
    
    Destinations toReach = new Destinations(toReachBytes);
    if (toReach.equals(thisDestination)) {
      /** You have reached the destination which had issued the node address
	  request */
      offset =1;
      int requestedLevel = nodeAddressResponse[offset++];
      byte[] idBytes = new byte[10];
      ProtocolID requestId = new ProtocolID(idBytes);
      System.arraycopy(nodeAddressResponse, offset, idBytes,0, 10);
      offset+=10;
      
      /** Unmarshall the address that was assigned for the node, corresponding
	  to the requestId*/
      byte[] addressAssignedBytes = new byte[4*(systemLevel+1)];
      System.arraycopy(nodeAddressResponse, offset, 
		       addressAssignedBytes, 0, 4*(systemLevel+1));
      offset+=(4*(systemLevel+1));
      NodeAddress addressAssigned = new NodeAddress(addressAssignedBytes);
      
      /** Unmarhsall the identify of the node which assigned the address */
      byte[] assignedByBytes = new byte[4*(systemLevel+1)];
      System.arraycopy(nodeAddressResponse, offset, 
		       assignedByBytes, 0, 4*(systemLevel+1));
      offset+=(4*(systemLevel+1));
      NodeAddress assignedBy = new NodeAddress(assignedByBytes);

      issueNodeAdditionSetupResponse(requestId, 
				     requestedLevel, 
				     addressAssigned,
				     assignedBy,
				     nodeAddress);
      return;
    }
    forwardNodeAddressResponse(nodeAddressResponse);
  }
  
  public void forwardNodeAddressResponse(byte[] nodeAddressResponse) {
    int offset = 
      1 /*header*/ + 1 /*requested Level*/ + 10 /*requestId */+
      4*(systemLevel+1) /** The assigned address */ + 
      4*(systemLevel+1) /** The address assigned by */;
    byte[] toReachBytes = new byte[4*(systemLevel+1)];
    System.arraycopy(nodeAddressResponse, offset, 
		     toReachBytes, 0, 4*(systemLevel+1));
    offset+= (4*(systemLevel+1));
    Destinations toReach = new Destinations(toReachBytes);
    byte[] traversedSoFarBytes = new byte[4*(systemLevel+1)];
    System.arraycopy(nodeAddressResponse, offset, 
		     traversedSoFarBytes, 0, 4*(systemLevel+1));
    Destinations traversedSoFar = new Destinations(traversedSoFarBytes);
    
    traversedSoFar.markAsTraversedAt(thisDestination);
    offset =  1 /*header*/ + 1 /*requested Level*/ + 10 /*requestId */+
      4*3*(systemLevel+1) /** The assigned-address, assigned-by, toReach*/;
    System.arraycopy(nodeAddressResponse, offset, 
		     traversedSoFar.getDestinationsInBytes(), 0, 
		     4*(systemLevel+1));
    //if (NodeAdditionProtocol_Debug)
      System.out.println("NAP:forwardNodeAddressResponse ->" + 
			 "ToReach" + toReach +
			 "TraversedSoFar" + traversedSoFar);
    sendToDestination(toReach, traversedSoFar, nodeAddressResponse);
  }
  
  public void forwardNodeAddressRequest(int requestedLevel, 
					ProtocolID requestId,
					Destinations origin,
					Destinations toReach,
					Destinations traversedSoFar) {
    byte[] nodeAddressRequest = 
      helper.constructNodeAdddressRequest(requestedLevel, systemLevel,  
					  requestId, origin, toReach, 
					  traversedSoFar);
    sendToDestination(toReach, traversedSoFar, nodeAddressRequest);
  }
  
  /** This method requires that the last 4*(systemLevel+1) bytes be 
      traversedSoFarBytes, we modify this information depending on the hop 
      that we send this information over */
  private void sendToDestination(Destinations toReach, 
				 Destinations traversedSoFar,
				 byte[] byteStream) {
    Gateway[] hopsToTake =
      gatewayInfo.hopsToReachDestination(toReach, traversedSoFar);
    if (hopsToTake != null) {
      for (int i=0; i < hopsToTake.length; i++) {
	int hopLevel = hopsToTake[i].getLevel();
	int requestedLevel = byteStream[1];
	if (hopLevel > requestedLevel)
	  continue;
	
	byte[] stream =  getStreamToSendOverLevel(hopLevel,
						  traversedSoFar,
						  byteStream);
	protocolHandler.sendToNode(hopsToTake[i], stream);
	System.out.println("NodeAddition: Byte stream sent over hop " + 
			   hopsToTake[i]);
      }
      return;
    } else {
      System.out.println(" NodeAdditionProtocol: - No hops to take");
    }
  }
  
  private synchronized byte[] 
  getStreamToSendOverLevel(int hopLevel, 
			   Destinations traversedSoFar,
			   byte[] byteStream) {
    int offset = byteStream.length - 4*(systemLevel+1);
    byte[] traversedBytes = 
      traversedSoFar.prepareDestinationsToSendOverLevel(hopLevel);
    byte[] stream = new byte[byteStream.length];
    System.arraycopy(byteStream, 0, stream, 0, byteStream.length);
    System.arraycopy(traversedBytes, 0,
		     stream ,offset, 4*(systemLevel+1));
    return stream;
  }
 
  /**
     Process the node addition request. This method is responsible for
     <li> Assigning the address, if it is allowed to do so.
     <li> Forward the request to assign addresses.
     @param node - The unassigned node from which this request was received.
     @param connectionRequestPacket - The request for set up.
  */
  public void processNodeAdditionRequest(String node, 
					 byte[] connectionRequestPacket) {
    if (connectionRequestPacket.length !=2) {
      System.out.println("Malformed reqest for adding unit");
      return;
    }

    /** Add this request to the connection request table */
    ProtocolID requestId = protocolHandler.getUniqueProtocolID();
    if (requestId == null) {
      System.out.println("NodeAdditionProtocol:: Trying to handle a node "
			 + " addition request without an assigned address ");
      return;
    }
    /** Add the connection request into the table. This enables us to know
	the exact node which needs to be remapped in the protocolHandler and
	the transportHandler */
    connectionRequestTable.put(requestId, node);


    /** The flag which keeps track of the to/from relationship being sought */
    boolean from=false;
    /** This is the initializer for the connectionRequest level */
    int requestedLevel=0;
    
    int connectionRequest = connectionRequestPacket[1];
    if ( (connectionRequest & 1) == 1) 
      from = true;
    /** In the case of from/to relationships if it seeks to be more than the
	level-0 gateway, traverse the connectivity graph and decide which unit
	to pick to issue a connection request */
    connectionRequest >>= 1;
    System.out.println("Connection Request " + 
		       ByteUtilities.printByte((byte)connectionRequest));
    
    for (int i =1; i < 7; i++) {
      if ( (connectionRequest &1) == 1) {
	requestedLevel = i;
	break;
      }
      connectionRequest >>= 1;
    }
    
    System.out.println("Node " + node + " Seeks to be a level (" + 
		       requestedLevel + ") gateway in the system with level " +
		       systemLevel);
    
    Destinations addressDestination = 
      generateDestinationForNodeAdditionAtLevel(requestedLevel);
    
    /** This is to handle the case where a request has been received by a node
	which can handle the address generation too */
    if (addressDestination.equals(thisDestination)) {
      System.out.println("Inside node responsible for addr generation ");
      /** This is the node which is responsible for assigning the 
	  addresses at level = requestedLevel */
     
      /**Put check here to see if the number of units exceeds 31*/
      //numberOfUnitsAtLevel[requestedLevel]++;  
      //System.out.println("Num Of units at level (" +  requestedLevel + 
      //")->" +  numberOfUnitsAtLevel[requestedLevel]);
      //addressDestination.setDestinationsAtLevel(requestedLevel, 
      //numberOfUnitsAtLevel[requestedLevel]);
      
      int assignedUnitId = assignAddressForUnitAtLevel(requestedLevel);
      addressDestination.setDestinationsAtLevel(requestedLevel, 
						assignedUnitId );
      if (requestedLevel > 0) {
	for (int i = requestedLevel-1; i >= 0; i--) {
	  addressDestination.setDestinationsAtLevel(i, 1);
	}
      }
      
      //byte[] addressBytes = addressDestination.getDestinationsInBytes();
      //NodeAddress addressAssigned = new NodeAddress(addressBytes);
      NodeAddress addressAssigned = 
	new NodeAddress(addressDestination.getDestinationsInBytes());
      issueNodeAdditionSetupResponse(requestId, 
				     requestedLevel, 
				     addressAssigned,
				     nodeAddress,
				     nodeAddress);
      return;
    }
    Destinations traversedSoFar = new Destinations(systemLevel);
    traversedSoFar.markAsTraversedAt(thisDestination);
    forwardNodeAddressRequest(requestedLevel, requestId, 
			      thisDestination, addressDestination, 
			      traversedSoFar);
    System.out.println("Forwarding node Addition request to" + 
		       addressDestination);
  }



  private Destinations 
  generateDestinationForNodeAdditionAtLevel(int requestedLevel) {
    /**We are now constructing the destination that is best suited to deal 
       with this node address request */
    Destinations dest =  new Destinations(systemLevel);
    for (int i= requestedLevel+1 ; i < systemLevel+1; i++) {
      dest.setDestinationsAtLevel(i, 
				  thisDestination.getDestinationsAtLevel(i));
    }
    System.out.println("First half of destination computed ");
    
    for (int i = requestedLevel; i >= 0; i--) {
			if (thisDestination.getDestinationsAtLevel(i) == 
				gatewayInfo.getLowestNumberedUnitAtLevel(i)) {
				dest.setDestinationsAtLevel(i,
						thisDestination.getDestinationsAtLevel(i));
				System.out.println(" Continuing ..");
				continue;
				/** Continue with the calulation of lowest numbered unit. */
			}
			if (NodeAdditionProtocol_Debug) {
				System.out.println(" Dest At level (" + i + ") ="
											+ thisDestination.getDestinationsAtLevel(i)
											+ " Lowest numbered unit "
											+ gatewayInfo.getLowestNumberedUnitAtLevel(i));
			}
			dest.setDestinationsAtLevel(i, gatewayInfo.getLowestNumberedUnitAtLevel(i));
			System.out.println(" Breaking out ..");
			break;
			/** Exit out - The destination cannot be zeroed down anymore */
		}
		return dest;
  }


  private void 
  issueNodeAdditionSetupResponse(ProtocolID requestId, 
				 int requestedLevel, 
				 NodeAddress addressAssigned,
				 NodeAddress assignedBy,
				 NodeAddress connectedTo) {
    
    if (!connectionRequestTable.containsKey(requestId)) {
      System.out.println("NodeAdditionProtocol: The connectionRequestTable " +
			 " has no reference to the requestID " + requestId);
      return;
    }
    int offset=0;
    byte[] nodeAdditionResponse = 
      helper.constructNodeAdditionResponse(requestId, requestedLevel,
					   systemLevel, addressAssigned,
					   assignedBy, connectedTo);
    /** Construct connection information and map the unassigned node at
	the protocolHandler level. This helps us communicate using the
	constructed gateway information */
    int[] addressAssgnInts =  addressAssigned.getAddressInInts();
    int[] addressConstruct = { addressAssgnInts[systemLevel-requestedLevel] };
    NodeAddress nodeOne = new NodeAddress(addressConstruct);
    int levelOne = requestedLevel;
    /*Changed to fix diff between destinations and node address */
    //int[] addressConstructTwo = { thisDestination.getDestinationsAtLevel(0)};
    //NodeAddress nodeTwo = new NodeAddress( addressConstructTwo); */
    int[] addressInts = nodeAddress.getAddressInInts();
    int[] addressConstructTwo = {addressInts[systemLevel]};
    NodeAddress nodeTwo = new NodeAddress( addressConstructTwo);
    /*******************FIX Complete*******************/
    System.out.println(moduleName + "issueSetupResponse() " +
		       "NodeOne->" + nodeOne + " LevelOne->" + levelOne +
		       " NodeTwo->" + nodeTwo + " LevelTwo->" + 0 );
    Connection createdConnection = new Connection(nodeAddress, 
						  nodeOne, levelOne,
						  nodeTwo, 0, requestedLevel,
						  requestId);
    //gatewayInfo.addConnection(createdConnection);
    Gateway hop = new Gateway(gatewayInfo.getVertexNode(), 
			      nodeOne, (short)levelOne);
    String node = (String) connectionRequestTable.get(requestId);
    protocolHandler.mapLinkToBrokerNode(node, hop, requestId);
    protocolHandler.sendToNode(hop, nodeAdditionResponse);
    protocolHandler.propagateConnectionInformation(createdConnection, hop);
    protocolHandler.propagateProfilesAtSpecifiedLevel(hop, requestedLevel);
  }
  
  /** This method processes the Node Addition Response. The information
      contained in the nodeAdditionResponse provides indications regarding the
      status of the node addition request. In the event that this request
      was successfully completed, the node address of the node in question is
      provided. 
      @param nodeAdditionResponse The response to a node/unit addition
      request.
  */
  public void processNodeAdditionResponse(String node,
					  byte[] nodeAdditionResponse) {
    if (nodeAdditionResponse[1] != 0) {
      System.out.println("The node addition was a failure ");
    }
    int offset = 2;
    /** Retrieving the unique request ID */
    byte[] idBytes = new byte[10];
    System.arraycopy(nodeAdditionResponse, offset, idBytes, 0, 10);
    ProtocolID requestId = new ProtocolID(idBytes);
    offset +=10;
    
    int requestedLevel = nodeAdditionResponse[offset++];
    systemLevel    = nodeAdditionResponse[offset++];


    addressBytes = new byte[4*(systemLevel+1)];
    System.arraycopy(nodeAdditionResponse, offset, 
		     addressBytes, 0, 4*(systemLevel+1));
    nodeAddress = new NodeAddress(addressBytes);
    thisDestination = new Destinations(addressBytes);
    offset += (4*(systemLevel+1));
    
    /** This call also ensures the initialization of the connectivity graph */
    protocolHandler.setAssignedAddress(nodeAddress);
    
    byte[] addressAssignedByBytes =  new byte[4*(systemLevel+1)];
    System.arraycopy(nodeAdditionResponse, offset, 
		     addressAssignedByBytes, 0, 4*(systemLevel+1));
    offset += (4*(systemLevel+1));

    byte[] connectedToBytes = new byte[4*(systemLevel+1)];
    System.arraycopy(nodeAdditionResponse, offset, 
		     connectedToBytes, 0, 4*(systemLevel+1));
    /** Construct connection information and map the unassigned node at
	the protocolHandler level. This helps us communicate using the
	constructed gateway information */
    //int[] addressConstructOne = { thisDestination.getDestinationsAtLevel(0)};
    /*Changed to fix diff between destinations and node address */
    //byte[] addressConstructOne = 
    //  ByteUtilities.getBytes(thisDestination.getDestinationsAtLevel(0));
    //NodeAddress nodeOne = new NodeAddress( addressConstructOne);
    int[] addressInts = nodeAddress.getAddressInInts();
    int[] addressConstructOne = {addressInts[systemLevel]};
    NodeAddress nodeOne = new NodeAddress( addressConstructOne);
    /*** FIX Complete *******************************************/

    NodeAddress addressAssignedBy = new NodeAddress(addressAssignedByBytes);
    NodeAddress connectedTo = new NodeAddress(connectedToBytes);
    int[] connectedToInts =  connectedTo.getAddressInInts();
    int[] addressConstruct = { connectedToInts[systemLevel-requestedLevel] };
    NodeAddress nodeTwo = new NodeAddress(addressConstruct);
    int levelTwo = requestedLevel;
   
    Connection createdConnection = new Connection(nodeAddress,
						  nodeOne, 0,
						  nodeTwo, levelTwo, 
						  requestedLevel,
						  requestId);
    
    Gateway hop = new Gateway(gatewayInfo.getVertexNode(), 
			      nodeTwo, (short)levelTwo);
    
    protocolHandler.mapLinkToBrokerNode(node, hop, requestId);
    System.out.println("The node addition response was received for the " +
		       " Requested Level = " + requestedLevel + 
		       " The System Level = "+ systemLevel + 
		       " The node address " + nodeAddress + 
		       " was assigned by the node " + addressAssignedBy +
		       " NodeOne " + nodeOne +
		       " Nodetwo " + nodeTwo);
    gatewayInfo.addConnection(createdConnection);
  }
  
  
  /** This method is responsible for constructing the node addition request.
      A node specifies the kind of gateways it seeks to be in the integer
      array, while specifying the <i>to/from</i> charachteristics of the 
      node addition request 
      @param levels - Integer array specifying the kind of gateways the node
      seeks to be.
      @param from - Speicification of the <i>to/from</i> relationship for
      node addition.
  */
  public byte[] constructNodeAdditionRequest(int[] levels, boolean from) {
    return helper.constructNodeAdditionRequest(levels, from);
  }
  


   /** This method assigns the best available address at a certain level. If the
      biggest position with a "1" bit has reached the limit of 32, we see if
      there are any slot i.e. 0-bits, available for reusing failed node 
      addresses. This method returns a "0" if neither of these conditions are
      met. The node should then prepare to send a failure response notifying
      the requestee of the failed request. */
  private int assignAddressForUnitAtLevel(int level) {
    Destinations completeDestinations =
      gatewayInfo.getListOfDestinationsKnownToNode();
    completeDestinations.markAsTraversedAt(thisDestination);
    int assignedAddress = 0;

    int nodesAtLevel = completeDestinations.getDestinationsAtLevel(level);

    int position = ByteUtilities.getLastOneBitPosition(nodesAtLevel);
    
    if (position <= 31) {
      assignedAddress = position+1;
      assignedAddress = 1 << (assignedAddress -1);
      System.out.println("\n\nAssigned Address = " + assignedAddress + 
			 ", Position = " + position + "\n");
      return assignedAddress;
    } 
    
    
    /** Now if the position returned in greater than 32, we have to try and
	locate the first valid position where there is a "0" bit. If this 
	position turns out to be 'zero' it means all the positions have been
	taken up and no reuse of slots is possible. We should then propagate
	an error message to this effect */
    position = 0; /* resetting posiition */
    
    position = ByteUtilities.getFirstZeroBitPosition(nodesAtLevel);
    
    if (position > 0) {
      /** This means that we have been able to successfully reuse a slot that
	  was available */
      assignedAddress = position;
    }
    
    assignedAddress = 1 << (assignedAddress -1);
    System.out.println("\n\nAssigned Address = " + assignedAddress + 
		       ", Position = " + position + "\n");
    

    return assignedAddress;
  }/*end assignAddressForUnitAtLevel() method */
  



  public static void main(String[] args) {
    System.out.println("Testing the Node Additon Protocol");
    int[] levels = {1,3};
    NodeAdditionProtocol nodeProtocol = new NodeAdditionProtocol();
    nodeProtocol.processNodeAdditionRequest("testnode", 			    nodeProtocol.constructNodeAdditionRequest(levels, true));
  }
}

